<?php
/**
 * Classe controller.
 *
 * Toutes les classes métiers doivent hériter de cette classe.
 * PHP version 5.1.6
 *
 * @package   Smartbok
 * @author    Christophe Hebet <christophe.hebet@dgfip.finances.gouv.fr>
 * @copyright 2013 - DGFiP
 */

/**
 * Classe smartbokController, classe mère de toutes les classes métier
 */
class SmartbokController
{
    const SBK_ACCESS_INTERDIT = 'L\'accès a cette fonctionnalité n\'est pas autorisé';
    /**
     * Objet requete (entrée des données)
     *
     * @var object (Requete)
     */
    protected $_requete;
    /**
     * Objet Reponse (sortie des données)
     *
     * @var objet (Reponse)
     */
    protected $_reponse;
    /**
     * Titre de la page appellé (s'insère dans la balise title du head)
     *
     * @var string
     */
    protected $_page = '';
    protected $_formulaireInsererDonneesPostees;
    protected $_formulaireAfficherChampsErreur;
    protected $_listeChampErreur = null;
    protected $_profilsAutorises = null;
    protected $_referers = null;
    public $_accessOk = false;
    protected $traiterAffichage = true;
    protected $params = null;

    /**
     * Méthode statique appelé par le controlleur frontal (FrontController).
     * Cette méthode vérifie l'existence de la
     * classe dans les pile d'appel (require + autoload). En cas de succès, la méthode instancie la classe et exécute
     * la méthode lancer.
     * Si une classe a été définie et que l'instanciation a généré une erreur, la méthode retourne une erreur.
     * Si aucune classse n'a été définie, la méthode redirige sur la page d'accueil.
     *
     * @param object $requete
     * @param object $reponse
     * @return string
     */
    public static function executer($requete, $reponse) {
        $template = 'index.tpl.php';
        $accessOk = false;
        $class = $requete->controller;
        if (Authentification::testAuthentification($requete->getAppVars()['baseUri']) === true) {
            $accessOk = true;
        }
        else {
            if (isset($_SESSION['authentification']) === true && $_SESSION['authentification'] === 'KO') {
                unset($_SESSION['authentification']);
                $reponse->addVar('agenda', Model_WotHub::getOpenAgenda());
                $reponse->addVar('statAccueil', Model_WotHub::getStatAccueil());
            }
            else {
                $_SESSION['authentification'] = 'KO';
                header('location:'.$requete->getAppVars()['baseUri']);
            }
        }
        if (class_exists($class, true) === true) {
            $controller = new $class($requete, $reponse); 
            $controller->params = $controller->getParams();
            $controller->_accessOk = $accessOk; 
            return $controller->lancer();
        }
        else {
            if (isset($class) === true && $class !== false) { // chargement d'une classe inexistante
                echo 'class inexistante';
            }
            else { // aucun paramètre ->REDIRECTION ACCUEIL
                $controller = new SmartbokController($requete, $reponse);
                $controller->_accessOk = $accessOk;
                $controller->addTemplate($template);

                if (isset($_SESSION['authentification']) === true && $_SESSION['authentification'] === 'ok') {
                    $controller->pseudo = $_SESSION['accountInfo']['pseudo'];
                    $controller->clanInfo = $_SESSION['clanInfo'];
                }
                
                $controller->statAccueil = Model_WotHub::getStatAccueil();
                $controller->agenda = Model_WotHub::getOpenAgenda();
                $controller->_render('smartbok/vues/smartbok.tpl.php');
                
                return $controller->_reponse->display();
            }
        }
    }

    /**
     * Constructeur de la classe.
     *
     * @param object $requete
     * @param object $reponse
     */
    public function __construct($requete, $reponse) {
        $this->_requete = $requete;
        $this->_reponse = $reponse;
    }

    /**
     * Méthode d'exécution des classes.
     * Cette méthode récupère l'action passée en paramètre de la requête.
     * Si l'action existe, elle est exécutée avant d'etre passée à la méthode d'affichage (_render).
     * Si l'action n'existe pas, une erreur est générée.
     *
     * @return boolean objet
     */
    public function lancer() {
        $action = $this->_requete->action;
        if ($this->_actionExists($action) === false) {
            echo 'la méthode ('.$action.') n\'existe pas : ';
            return false;
        }
        // executer les action pre actions
        $this->$action();
        if ($this->traiterAffichage === false) {
            return '';
        }
        $this->_render('smartbok/vues/smartbok.tpl.php');
        $html = $this->_reponse->display();
        // exécuter les action de modification de la réponse. (controle sécurité, ajout d'éléments etc...)
        $doc = new DOMDocument('1.0', 'UTF-8');
//         echo $html;
        $html = iconv('UTF-8', 'UTF-8//IGNORE', $html);
        $html = str_replace('&', '&amp;', $html);
        $doc->loadXML($html, LIBXML_NOWARNING);
        // $this->controlerAcces();
        $doc = $this->ajouterJetonCSRF($doc);
        $doc = $this->ajouterEncodageFormulaire($doc);
        $doc = $this->injecterDonneesPosteesFormulaires($doc);
        $doc = $this->afficherChampsErreurFormulaire($doc);
        $html = $doc->saveXML();
        return $html;
    }

    /**
     * Méthode pour ajouter un template à l'instance du controller.
     *
     * @param string $template
     */
    public function addTemplate($template) {
        $this->_reponse->addTemplate($template);
    }

    /**
     * Méthode d'affectation des variables à l'isntance de l'objet.
     *
     * @param string $name
     * @param variable $value
     */
    public function __set($name, $value) {
        $this->_reponse->addVar($name, $value);
    }

    /**
     * méthode d'ajout des variabels javascripts nécessaire en entete de la page
     * @param string $varName
     * @param string $varValue
     */
    public function addJsVar($varName, $varValue) {
        $this->_reponse->addJsVar($varName, $varValue);
    }
    
    /**
     * Méthode de lecture des variables de l'instance de l'objet.
     *
     * @param string $name
     */
    public function __get($name) {
        return $this->_reponse->getVar($name);
    }

    /**
     * Méthode de controle de l'existence de la méthode.
     * La méthode instancie un objet ReflectionMethod afin de controller les attributs de la méthode appellée.
     * Si la méthode existe et n'est ni privée ni le constructeur, la fonction retourne true sinon elle retour false.
     *
     * @param string $action
     * @return boolean
     */
    private function _actionExists($action) {
        try {
            $method = new ReflectionMethod(get_class($this), $action);
            return ($method->isPublic() && ! $method->isConstructor());
        }
        catch (Exception $e) {
            return false;
        }
    }

    /**
     * Méthode de gestion de l'affichage.
     * La variable en paramètre est le nom du template.
     * Cette méthode instancie un objet vue, recalcule le chemin des ressources, pour le cas de la réécriture d'url.
     * Elle ajoute le titre de la page, puis prépare l'affichage.
     *
     * @param string $file
     */
    private function _render($file) {
        $this->accessOk = $this->_accessOk;
        $vue = new Vue();
        $this->calculerCheminRessources();
        $this->titrePage = $this->_page;
        $this->_reponse->preparer($vue->render($file, $this->_reponse->getVars()));
    }

    /**
     * Méthode de recalcul des chemins pour les ressources (images, css, scripts)
     * affecte le résultat à une variable.
     */
    protected function calculerCheminRessources() {
        $count = '';
        if ($this->urlRewriting === true) {
            if (isset($this->_requete->controller) === true && $this->_requete->controller !== false) {
                $count = str_repeat('../', count($this->_requete->params) + 1);
            }
        }
        $this->cheminRes = $count;
    }

    /**
     * Méthode permettant d'ajouter un javascript ponctuellement à une page. ou une css
     *
     * @param string $nomScript
     */
    protected function ajouterScript($nomScript, $type= 'js') {
        $this->_reponse->ajouterVariableApplicative($nomScript, null, $type);
    }
    
    /**
     * Cette péthode permet de controler l'accès aux fonctionnalités et ainsi eviter qu'un utilisateur arrive sur
     * une page a laquelle il n'a pas droit
     */
    protected function controlerAcces() {
        $controles = array('controlerProfil', 'controlerReferer', 'controlerEntete');
        if ($this->securite['controleAccessAuto'] === true) {
            foreach($controles as $controle) {
                $retour = $this->$controle();
                if ($retour === false) {
                    SmartbokMessage::addMessage(SmartbokMessage::ERR, self::SBK_ACCESS_INTERDIT);
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Méthode de contrôle es entêtes;
     * Cette méthode eprmet de vérifier que le dn transmis dans l'entête et bien le même que celui stocké en session.
     * Le contrôle se fait sur al rpésence de la variable Authorization dans la session
     *
     * @return boolean
     */
    protected function controlerEntete() {
        if ($this->securite['controleEntete'] === true) {
            $headers = getallheaders();
            if (isset($headers['Authorization']) !== true) {
                return false;
            }
            else {
                $authorization = base64_decode(substr($headers['Authorization'], 6));
                $dn = substr($authorization, 0, strpos($authorization, ':'));
                if (isset($_SESSION['Authorization']) === true &&  $_SESSION['Authorization'] === $dn) {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Méthode de controle des referer.
     * Cette méthode nécessite la présence de la variable baseUri dans le fichier de config. Cette variable va
     * permettre de simplifier le contrôle des referer.
     * Si les referer ne sont pas définis dans une array, la méthode interdit l'accès à la fonctionnalité.
     *
     * @return boolean
     */
    protected function controlerReferer() {
        if ($this->securite['controleReferer'] === true) {
            if (isset($_SERVER['HTTP_REFERER']) === false) {
                return false;
            }
            $uri = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
            if($_SERVER['HTTP_REFERER'] === $uri) {
                return true;
            }
            if (is_array($this->_referers) === true && count($this->_referers) > 0) {
                $referer = str_replace($this->baseUri, '', $_SERVER['HTTP_REFERER']);
                if (in_array($_SERVER['HTTP_REFERER'], $this->_referers) === false &&
                    in_array($referer, $this->_referers) === false) {
                    return false;
                }
            }
            else {
                return false;
            }
        }
        return true;
    }

    protected function controlerProfil() {
        $retour = false;
        if ($this->securite['controleProfil'] === true) {
            if (is_array($this->_profilsAutorises) === true && count($this->_profilsAutorises) > 0) {
                $profilsDetenus = $this->menu['profils'];
                foreach ($this->_profilsAutorises as $profil) {
                    if (in_array($profil, $profilsDetenus) === true) {
                        $retour = true;
                        break;
                    }
                }
            }
        }
        else {
            $retour = true;
        }
        return $retour;
    }

    /**
     * Méthode d'ajout d'un champ caché contenant le jeton csrf.
     * Le Jeton est généré dynamiquement et aléatoirement.
     * Ce jeton est ajouté à la session lors de cette insertion.
     *
     * @param object $doc
     * @return string
     */
    protected function ajouterJetonCSRF($doc) {
        if ($this->securite['ControleCsrf'] === false) {
            return $doc;
        }

        if (function_exists('openssl_random_pseudo_bytes')) {
            $ssl = true;
            $jetonVal = openssl_random_pseudo_bytes(512, $ssl);
        }
        else {
            $jetonVal = uniqid('', true);
        }
        $jeton = $doc->createElement('input');
        $attributs = array(
            'name' => 'jetonCsrf',
            'type' => 'hidden',
            'value' => $jetonVal
        );
        foreach ($attributs as $attr => $value) {
            $attribut = $doc->createAttribute($attr);
            $attribut->value = $value;
            $jeton->appendChild($attribut);
        }
        $_SESSION['Csrf']['jeton'] = $jetonVal;
        $forms = $doc->getElementsByTagName('form');
        if ($forms->length > 0) {
            $form = $forms->item(0);
            $form->appendChild($jeton);
        }
        return $doc;
    }

    /**
     * Methode pour ajouté le charset de la config dans les attributs du formulaire
     *
     * @param object $doc
     *
     * @return $html
     */
    protected function ajouterEncodageFormulaire($doc) {
        if ($this->formulaires['forceEncodageCharset'] === false) {
            return $doc;
        }
        $charset = $doc->createAttribute('accept-charset');
        $charset->value = $this->charset;
        $forms = $doc->getElementsByTagName('form');
        if ($forms->length > 0) {
            $form = $forms->item(0);
            $form->appendChild($charset);
        }
        return $doc;
    }

    /**
     * méthode récursive pour rechercher un objet composant d'un formulaire a partir de son nom
     *
     * @param object $domElement
     * @param string $name
     * @param string $valeur
     * @return null object
     */
    private function _searchElement($domElement, $name, $valeur) {
        $champ = null;

        if ($domElement->hasChildNodes() === true) {
            foreach ($domElement->childNodes as $enfant) {
                $champ = $this->_searchElement($enfant, $name, $valeur);
                if (is_a($champ, 'DOMElement') === true) {
                    return $champ;
                }
            }
        }
        else if ($domElement->hasAttributes() === true) {
            if ($domElement->getAttribute('name') === $name) {
                if ($domElement->getAttribute('type') === 'radio') {
                    if ($domElement->getAttribute('value') === $valeur) {
                        $champ = $domElement;
                    }
                }
                else {
                    $champ = $domElement;
                }
            }
        }
        else {
            return null;
        }
        return $champ;
    }

    /**
     * méthode d'insertion des données postées dans le formulaire a afficher.
     * Attention cette méthode est faite pour fonctionner avec un formualrie dont les champs sont défini avec un id
     * conformément aux préconisations W3C.
     *
     * @param object $doc
     */
    protected function injecterDonneesPosteesFormulaires($doc) {
        if ($this->_formulaireInsererDonneesPostees === true) {
            // recherche a partir du nom du champ
            $forms = $doc->getElementsByTagName('form');
            $xpath = new DOMXPath($doc);
            foreach ($_POST as $nomChamp => $valeur) {
                if ($nomChamp === 'jetonCsrf') {
                    break;
                }
                $champs = null;
                if (is_array($valeur) === true) {
                    $nomChamp .= '[]';
                }
                $listeNoeuds = $xpath->evaluate("//*[@name='$nomChamp']");
                if ($listeNoeuds->length === 1) {
                    $champ = $listeNoeuds->item(0);
                }
                else {
                    foreach ($listeNoeuds as $node) {
                        if (is_array($valeur) === false) {
                            if ($node->getAttribute('value') === $valeur) {
                                $champ = $node;
                                break;
                            }
                        }
                        else {
                            $champ = $node;
                            $champs = $listeNoeuds;
                        }
                    }
                }
                switch ($champ->nodeName) {
                    case 'textarea':
                        $champ->nodeValue = $valeur;
                    break;

                    case 'select':
                        foreach ($champ->childNodes as $option) {
                            if ($option->hasAttributes() === true) {
                                foreach ($option->attributes as $attr) {
                                    if ($attr->value === $valeur || in_array($attr->value, $valeur) === true) {
                                        $select = $doc->createAttribute('selected');
                                        $select->value = 'selected';
                                        $option->appendchild($select);
                                        if (is_array($valeur) === false) {
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    break;

                    default:
                        if (is_a($champs, 'DOMNodeList') === true) {
                            foreach ($champs as $node) {
                                $doc = $this->_traiterInput($doc, $node, $valeur);
                            }
                        }
                        else {
                            $doc = $this->_traiterInput($doc, $champ, $valeur);
                        }
                    break;
                }
            }
        }
        return $doc;
    }

    /**
     * Méthode de controle des éléments de type input, pour affectation de la valeur.
     *
     * @param object $doc
     * @param object $champ
     * @param string|array $valeur
     * @return unknown
     */
    private function _traiterInput($doc, $champ, $valeur) {
        switch ($champ->getAttribute('type')) {
            case 'checkbox':
                if (is_array($valeur) === true) {
                    if (in_array($champ->getAttribute('value'), $valeur) === true) {
                        $coche = $doc->createAttribute('checked');
                        $coche->value = 'checked';
                        $champ->appendChild($coche);
                    }
                }
                else {
                    $coche = $doc->createAttribute('checked');
                    $coche->value = 'checked';
                    $champ->appendChild($coche);
                }
            break;

            case 'radio':
                if ($champ->getAttribute('value') === $valeur) {
                    $coche = $doc->createAttribute('checked');
                    $coche->value = 'checked';
                    $champ->appendChild($coche);
                }
            break;

            default:
                $champ->setAttribute('value', $valeur);
            break;
        }

        return $doc;
    }

    /**
     * méhtode qui modifie le formulaire pour mettre en évidence les champs en anomalie.
     *
     * @param object $doc
     */
    protected function afficherChampsErreurFormulaire($doc) {
        if ($this->_formulaireAfficherChampsErreur === true) {
            if (is_array($this->_listeChampErreur) === true) {
                $labels = $doc->getElementsByTagName('label');
                if ($labels->length > 0) {
                    foreach ($labels as $label) {
                        if (in_array($label->getAttribute('for'), $this->_listeChampErreur) === true) {
                            $class = $doc->createAttribute('class');
                            $class->value = 'erreur';
                            $label->appendChild($class);
                        }
                    }
                }
            }
        }
        return $doc;
    }

    protected function _assignSession($tableau) {
        if (isset($_SESSION[$tableau]) === true) {
            foreach ($_SESSION[$tableau] as $var => $value) {
                $this->$var = $value;
            }
        }
    }
    
    protected function addProfils($profils) {
        $this->_reponse->addProfils($profils);
    }
    
    protected function getParams() {
        if (isset($this->_requete->params) === true) {
            return $this->_requete->params;
        }
        else {
            return null;
        }
    }
}